#!/usr/bin/python
#
# Copyright (c) 2010, Takashi Ito
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the authors nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import sys, re
from datetime import *

from trac.core import *
from trac.mimeview import Context
from trac.config import Option
#from trac.resource import IResourceManager
from trac.resource import Resource, ResourceNotFound
from trac.perm import IPermissionRequestor, PermissionError
from trac.web.chrome import add_stylesheet, add_script, add_ctxtnav, add_warning, Chrome
from trac.wiki.formatter import format_to_html, format_to_oneliner
from trac.util.datefmt import to_timestamp, to_datetime, utc, pretty_timedelta
from trac.util.html import html
from trac.util.text import to_unicode
from trac.util.translation import _
from trac.attachment import Attachment, AttachmentModule

from genshi.template import TemplateLoader
import formencode
from formencode import Invalid, validators

from testmanagement.model import *
from tracext import sa
from sqlalchemy import and_
from sqlalchemy.orm.exc import NoResultFound

if sys.version_info[:2] >= (2, 6):
    import json
else:
    import simplejson as json


node_type_map = {
    NODE_TYPE_PROJECT: (Project, 'proj', {'enable_on_project': True}),
    NODE_TYPE_TESTSUITE: (TestSuite, 'testsuite', {'enable_on_testsuite': True}),
    NODE_TYPE_TESTCASE: (TestCase, 'testcase', {'enable_on_testcase': True}),
}


class TestManagementSystem(Component):

    implements(IPermissionRequestor)

    _perm_actions = [
        'TEST_VIEW',
        'TEST_CREATE',
        'TEST_EDIT',
        'TEST_DELETE',
        'TEST_ADMIN',
    ]

    # IPermissionRequestor methods
    def get_permission_actions(self):
        return self._perm_actions

    # IResourceManager methods
    #def get_resource_realms(self):
    #    yield 'test'

    #def get_resource_description(self, resource, format=None, context=None, **kwargs):
    #    print '####### get_resource_description() called'

    def process(self, req):
        self.data = {}
        action = req.args.get('action')

        #add_ctxtnav(context.req, 'Test Home', context.req.href.test())
        #add_ctxtnav(context.req, 'Execute', context.req.href.test())
        #add_ctxtnav(context.req, 'Result', context.req.href.test())

        if action == 'newproject':
            return self._process_newproject(req)

        if action == 'projects':
            return self._process_projectlist(req)

        if action == 'spec':
            return self._process_spec(req)

        if action == 'tree':
            return self._process_tree(req)

        if action == 'detail':
            return self._process_detail(req)

        if action == 'part':
            return self._process_part(req)

        if action == 'newsuite':
            return self._process_newsuite(req)

        if action == 'newtestcase':
            return self._process_newtestcase(req)

        if action == 'customfields':
            return self._process_customfields(req)

        if action == 'customfield':
            return self._process_customfield(req)

        if action == 'newcustomfield':
            return self._process_newcustomfield(req)

        req.redirect(req.href.test())

    #--------------------------------
    def _get_customfieldvalues(self, session, node, **kwargs):
        parent = node
        while parent.parent:
            parent = parent.parent
        customfields = session.query(CustomField, CustomFieldValue)\
            .filter_by(**kwargs)\
            .filter_by(project_id=parent.id)\
            .outerjoin((CustomFieldValue,
                and_(CustomFieldValue.node_id==node.id,
                     CustomField.id==CustomFieldValue.cfield_id)))
        customfields = [(x, y) for x, y in customfields]
        return customfields

    def _get_customfields(self, session, node, **kwargs):
        parent = node
        while parent.parent:
            parent = parent.parent
        customfields = session.query(CustomField)\
            .filter_by(**kwargs)\
            .filter_by(project_id=parent.id)
        customfields = [(x, None) for x in customfields]
        return customfields

    def _store_customfields(self, customfields, req, node_id=None):
        for n, (field_spec, field_value) in enumerate(customfields):
            # create or update custom field value
            value = req.args.get('customfield_%i' % field_spec.id, '')
            if field_value:
                field_value.value = value
            else:
                field_value = CustomFieldValue(node_id, field_spec.id, value)
                field_value.customfield = field_spec
            # store it
            field_value.validate(req)
            customfields[n] = (field_spec, field_value)

    def _store_checkitems(self, node, req):
        args = req.args
        ids = filter(lambda x: x.startswith("check_id_") and x[9:].isdigit(), args)
        ids = map(lambda x: int(x[len("check_id_"):]), ids)

        # remove orphans
        carried = [int(x) for x in map(lambda x: args.get('checkitem_id_%i' % x, ''), ids) if x.isdigit()]
        orphans = [x for x in node.checkitems if x.id not in carried]
        for o in orphans:
            node.checkitems.remove(o)

        # add or update checkitems
        for n in sorted(ids):
            id = args.get('checkitem_id_%i' % n, '')
            check_id = args.get('check_id_%i' % n)
            check_point = args.get('check_point_%i' % n)
            if id.isdigit():
                checkitem = node.get_checkitem(int(id))
                checkitem.check_id = check_id
                checkitem.check_point = check_point
            else:
                checkitem = CheckItem(node.id, check_id, check_point)
                node.checkitems.append(checkitem)

    def _process_part(self, req, part=True):
        req.perm.require('TEST_VIEW')
        session = sa.session(self.env)
        id = req.args.get('id')
        preview = False

        # get node information
        node = session.query(Node).filter_by(id=id).one()
        try:
            node_cls, node_name, kw = node_type_map[node.node_type]
        except KeyError, e:
            self.log.error('Unknown node type: %i' % node.node_type)
            # TODO: raise appropriate exception
            raise
        target = session.query(node_cls).filter_by(node_id=id).one()
        self.data[node_name] = target

        # get custom fields
        customfields = self._get_customfieldvalues(session, node, **kw)
        self.data['customfields'] = customfields

        # create context
        context = Context.from_request(req, Resource('test', id=id))

        if req.method == 'POST':
            if req.args.get('fmt') == 'edit':
                req.perm.require('TEST_EDIT')
                preview = 'preview' in req.args
                target.apply(req.args)
                # store custom fields
                self._store_customfields(customfields, req, id)
                self._store_checkitems(target.node, req) # TODO
                target.validate(req)
                
                if not preview and not req.chrome['warnings']:
                    for f, value in customfields:
                        if value not in node.cfield_values:
                            node.cfield_values.append(value)
                        session.add(value)
                    target.node.modified = to_timestamp(datetime.now(utc))
                    session.add(target)
                    session.commit()
                    req.redirect(req.href.test(id, part and 'part' or None))

            elif req.args.get('fmt') == 'delete':
                req.perm.require('TEST_DELETE')
                # delete attachment files
                db = self.env.get_db_cnx()
                Attachment.delete_all(self.env, 'test', target.node.id, db)
                # TODO: improve the message
                title = None
                msg = '%s was successfully deleted.' % node.name
                session.delete(target)
                session.commit()
                return 'message.html', {'message': msg, 'title': title}, None

        self.data['attachments'] = AttachmentModule(self.env).attachment_data(context)
        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'context': context,
            'preview': preview,
            'page': part and 'part' or '',
            'part': part,
            'fmt': req.args.get('fmt'),
            'get_field_type_name': get_field_type_name,
        })
        req.send_header('Cache-Control', 'no-cache')
        return 'detail.html', self.data, None

    def _process_detail(self, req):
        return self._process_part(req, part=False)

    def _process_tree(self, req):
        session = sa.session(self.env)
        id = req.args.get('id')

        nodes = []
        for node in session.query(Node).filter_by(parent_id=id):
            tmp = {
                'attributes': {
                    'id': 'node_%i' % node.id,
                    'node_id': node.id,
                    'class': (node.node_type == NODE_TYPE_TESTCASE) and 'testcase' or 'testsuite',
                },
                'data': node.name,
            }
            if node.node_type == NODE_TYPE_TESTSUITE:
                tmp['state'] = 'closed'
            nodes.append(tmp)

        body = json.dumps(nodes)
        req.send_response(200)
        req.send_header('Content-type', 'text/plain')
        req.send_header('Content-Length', str(len(body)))
        req.send_header('Cache-Control', 'no-cache')
        req.end_headers()
        req.write(body)

    def _process_spec(self, req):
        req.perm.require('TEST_VIEW')
        session = sa.session(self.env)
        id = req.args.get('id')
        try:
            node = session.query(Node).filter_by(id=id).one()
        except NoResultFound, e:
            # TODO: handle exception orm_exc.NoResultFound
            raise

        add_script(req, 'test/js/jquery.ui.all.js')
        add_script(req, 'test/js/jquery.layout.min.js')
        add_script(req, 'test/js/jquery.tree.min.js')
        add_script(req, 'test/js/jquery.form.js')
        add_stylesheet(req, 'common/css/code.css')
        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'node': node,
        })
        return 'tree.html', self.data, None

    def _process_projectlist(self, req):
        req.perm.require('TEST_VIEW')
        session = sa.session(self.env)
        projects = session.query(Project).filter(Project.node.has(Node.active==1))
        self.data.update({
            'projects': projects,
        })
        add_stylesheet(req, 'test/css/testmanagement.css')
        return 'project_list.html', self.data, None

    def _process_newproject(self, req):
        req.perm.require('TEST_CREATE')
        preview = False

        if req.method == 'POST':
            preview = 'preview' in req.args
            project = Project.build(req.args)
            project.validate(req)

            if not preview and not req.chrome['warnings']:
                # create new project entry
                session = sa.session(self.env)
                session.add(project)
                session.commit()

                # redirect to the project list page
                req.redirect(req.href.test())
        else:
            project = Project(None, None, None)

        self.data.update({
            'project': project,
            'preview': preview,
        })
        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        return 'project.html', self.data, None

    def _process_newsuite(self, req, part=True):
        req.perm.require('TEST_CREATE')
        parent_id = req.args.get('id')
        preview = False

        # get custom fields
        session = sa.session(self.env)
        node = session.query(Node).filter_by(id=parent_id).one()
        customfields = self._get_customfields(session, node, enable_on_testsuite=True)
        self.data['customfields'] = customfields

        if req.method == 'POST':
            preview = 'preview' in req.args
            testsuite = TestSuite.build(req.args, parent_id=parent_id)
            # store custom fields
            self._store_customfields(customfields, req)
            testsuite.validate(req)

            if not preview and not req.chrome['warnings']:
                # create new test suite
                testsuite.cfield_values = [v for k, v in customfields]
                for k, v in customfields:
                    v.node = testsuite.node
                testsuite.node.modified = to_timestamp(datetime.now(utc))
                session.add(testsuite)
                session.commit()
                req.redirect(req.href.test(testsuite.node.id, 'part'))
        else:
            testsuite = TestSuite(None, None, parent_id)

        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'testsuite': testsuite,
            'preview': preview,
            'page': 'newsuite',
            'part': part,
            'fmt': 'edit',
            'get_field_type_name': get_field_type_name,
        })
        return 'detail.html', self.data, None

    def _process_newtestcase(self, req, part=True):
        req.perm.require('TEST_CREATE')
        parent_id = req.args.get('id')
        preview = False

        # get custom fields
        session = sa.session(self.env)
        node = session.query(Node).filter_by(id=parent_id).one()
        customfields = self._get_customfields(session, node, enable_on_testcase=True)
        self.data['customfields'] = customfields

        if req.method == 'POST':
            preview = 'preview' in req.args
            testcase = TestCase.build(req.args, parent_id=parent_id)
            # store custom fields and check items
            self._store_customfields(customfields, req)
            self._store_checkitems(testcase.node, req) # TODO
            testcase.validate(req)

            if not preview and not req.chrome['warnings']:
                # create new test case
                testcase.cfield_values = [v for k, v in customfields]
                for k, v in customfields:
                    v.node = testcase.node
                session.add(testcase)
                session.commit()
                req.redirect(req.href.test(testcase.node.id, 'part'))
        else:
            testcase = TestCase(None, None, parent_id)

        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'testcase': testcase,
            'preview': preview,
            'page': 'newtestcase',
            'part': part,
            'fmt': 'edit',
            'get_field_type_name': get_field_type_name,
        })
        return 'detail.html', self.data, None

    def _process_customfields(self, req):
        project_id = req.args.get('id')
        session = sa.session(self.env)
        self.data['customfields'] = session.query(CustomField).filter_by(project_id=project_id)
        self.data.update({
            'project_id': project_id,
            'field_types': FIELD_TYPES,
            'get_field_type_name': get_field_type_name,
        })
        add_stylesheet(req, 'test/css/testmanagement.css')
        return 'customfields.html', self.data, None

    def _process_customfield(self, req):
        req.perm.require('TEST_ADMIN')
        id = req.args.get('id')
        session = sa.session(self.env)
        customfield = session.query(CustomField).filter_by(id=id).one()

        if req.method == 'POST':
            if req.args.get('fmt') == 'delete':
                # TODO: confirm message
                session.delete(customfield)
                session.commit()
                req.redirect(req.href.test(customfield.project_id, 'customfields'))

            customfield.apply(req.args)
            customfield.validate(req)
            if not req.chrome['warnings']:
                session.add(customfield)
                session.commit()
                req.redirect(req.href.test(customfield.project_id, 'customfields'))

        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'customfield': customfield,
            'field_types': FIELD_TYPES,
        })
        return 'customfield_edit.html', self.data, None

    def _process_newcustomfield(self, req):
        req.perm.require('TEST_ADMIN')
        project_id = req.args.get('id')

        if req.method == 'POST':
            customfield = CustomField.build(req.args, project_id=project_id)
            customfield.validate(req)
            if not req.chrome['warnings']:
                session = sa.session(self.env)
                session.add(customfield)
                session.commit()
                req.redirect(req.href.test(customfield.project_id, 'customfields'))
        else:
            customfield = CustomField(project_id, None, None, None)

        add_stylesheet(req, 'test/css/testmanagement.css')
        add_script(req, 'common/js/folding.js') # 0.12
        Chrome(self.env).add_wiki_toolbars(req) # 0.12
        self.data.update({
            'customfield': customfield,
            'field_types': FIELD_TYPES,
        })
        return 'customfield_edit.html', self.data, None

